// 《围棋》作者版权所有。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package fs

import (
	"errors"
	"path"
)

// SkipDir用作WalkDirFuncs的返回值，表示将跳过调用中命名的目录。任何函数都不会将其作为错误返回
// 。
var SkipDir = errors.New("skip this directory")

// WalkDirFunc是WalkDir调用的函数类型，用于访问
// 每个文件或目录。
// 
// path参数包含WalkDir的参数作为前缀。
// 也就是说，如果使用根参数“dir”调用WalkDir，并在该目录中找到名为“a”的文件
// 则将使用
// 参数“dir/a”调用walk函数。
// 
// d参数是指定路径的fs.DirEntry。
// 
// 函数返回的错误结果控制WalkDir 
// 如何继续。如果函数返回特殊值SkipDir，则WalkDir 
// 跳过当前目录（如果d.IsDir（）为true，则跳过路径，否则跳过路径的父目录）。否则，如果函数返回非nil 
// 错误，WalkDir将完全停止并返回该错误。
// 
// err参数报告与路径相关的错误，表示
// WalkDir将不会进入该目录。该函数可以决定
// 如何处理该错误；如前所述，返回错误将导致WalkDir停止遍历整个树。
// 
// WalkDir在两种情况下使用非nil err参数调用函数。
// 
// 首先，如果根目录上的初始fs.Stat失败，WalkDir 
// 调用path设置为root、d设置为nil、err设置为
// 的函数，从fs.Stat返回错误。
// 
// 其次，如果目录的ReadDir方法失败，WalkDir调用
// 函数，将path设置为目录的path，将d设置为描述目录的
// fs.DirEntry，并将err设置为
// ReadDir中的错误。在第二种情况下，使用目录的
// 路径调用函数两次：第一次调用是在读取目录
// 之前尝试的，并且将err设置为nil，使函数有机会返回SkipDir并完全避免ReadDir。第二个调用是
// 在ReadDir失败后，从ReadDir报告错误。
// （如果ReadDir成功，则没有第二次调用。）
// 
// WalkDirFunc与filepath.WalkFunc之间的区别是：
// 
// -第二个参数的类型是fs.DirEntry，而不是fs.FileInfo。
// -在读取目录之前调用该函数，以允许SkipDir 
// 完全绕过读取的目录。
// -如果目录读取失败，则再次调用函数
// ，以便该目录报告错误。
// 
type WalkDirFunc func(path string, d DirEntry, err error) error

// walkDir递归地下降路径，调用walkDirFn。
func walkDir(fsys FS, name string, d DirEntry, walkDirFn WalkDirFunc) error {
	if err := walkDirFn(name, d, nil); err != nil || !d.IsDir() {
		if err == SkipDir && d.IsDir() {
			// 已成功跳过目录。
			err = nil
		}
		return err
	}

	dirs, err := ReadDir(fsys, name)
	if err != nil {
		// 第二次调用，报告ReadDir错误。
		err = walkDirFn(name, d, err)
		if err != nil {
			return err
		}
	}

	for _, d1 := range dirs {
		name1 := path.Join(name, d1.Name())
		if err := walkDir(fsys, name1, d1, walkDirFn); err != nil {
			if err == SkipDir {
				break
			}
			return err
		}
	}
	return nil
}

// WalkDir遍历根目录下的文件树，为树中的每个文件或
// 目录（包括根目录）调用fn。
// 
// 访问文件和目录时出现的所有错误均由fn:
// 过滤。有关详细信息，请参阅fs.WalkDirFunc文档。
// 
// 文件是按词法顺序遍历的，这使得输出具有确定性
// 但需要WalkDir将整个目录读入内存，然后再继续执行
// 遍历该目录。
// 
// WalkDir不跟随在目录中找到的符号链接，
// 但如果根本身是符号链接，则其目标将被遍历。
func WalkDir(fsys FS, root string, fn WalkDirFunc) error {
	info, err := Stat(fsys, root)
	if err != nil {
		err = fn(root, nil, err)
	} else {
		err = walkDir(fsys, root, &statDirEntry{info}, fn)
	}
	if err == SkipDir {
		return nil
	}
	return err
}

type statDirEntry struct {
	info FileInfo
}

func (d *statDirEntry) Name() string            { return d.info.Name() }
func (d *statDirEntry) IsDir() bool             { return d.info.IsDir() }
func (d *statDirEntry) Type() FileMode          { return d.info.Mode().Type() }
func (d *statDirEntry) Info() (FileInfo, error) { return d.info, nil }
